Release 10.1A: OpenEdge Development:
Progress Dynamics Advanced Development
Moving client logic support to an extended class
This chapter begins with a simple example with all client-side code in a custom super procedure for a single viewer. Realistically, much of that support code belongs in an extended viewer class, so that it can serve all viewers. This section illustrates the distinction between a single custom super procedure and a more generic procedure that serves a whole class of objects.
![]()
To extend the viewer class:
- Copy
src/adm2/custom/viewercustom.iandviewercustom.pto a local directory with the same relative path.- Edit
viewercustom.ito uncomment the line in the Main Block that starts the super procedureviewercustom.p,as shown:
![]()
- Move the generically useful code in your custom super procedure to your local copy of
viewercustom.p. These are all the support functions shown so far:Note: When writing code for a super procedure, always consider whether it will be attached to a single other procedure, or whether it can be attached to multiple other procedures.- Move the code that sets the variables where the lists of widget names and handles are stored. In the original
customersuper.pexample, this is done ininitializeobject. But will this still work when the code is in a super procedure that is part of an extension to the whole viewer class? No, it will not, because the newviewercustom.pprocedure might be supporting multiple viewers at once. As a result, these lists and any other viewer attributes that the code accesses must be re-established each time a request comes from a viewer. This is the first of several basic principles to keep in mind when creating a super procedure.In your
customersuper.p, the super procedure would only be attached to a single instance of one specific viewer, so it was sufficient to establish needed attribute values on startup ininitializeObject. In a more general-purpose procedure, you have to support switching the context of the object the code is supporting on each request, so that it will work properly when there is more than one object using the single super procedure instance. In this case, this means that the code to save off attribute values must go somewhere where it will be executed each time there is a request. In this case, this can be a local version of thedisplayFieldsprocedure, since therowDisplayprocedure that does any and all checks on display of a new row is called from there.So here is a version of
displayFieldsfor theviewercustom.pprocedure that gets the attribute values, saves them off, callsrowDisplay, and then resets the local variables so there is no danger of a stray call using stale values:
When you create this, define the variables
gcFieldsandgcHandlesin the Definitions section of the procedure so they are scoped to the procedure. Note that because of the principle of designing super procedures to support multiple other procedures, you must do this very cautiously. Generally, it is not good practice to have any variables or other constructs scoped to the super procedure itself, because the values might not apply to the next request of the super procedure, which might come from a different object. In this case, it is something of an optimization to write the code in such a way that, within the context of a single call todisplayFields, all references to these variables will be valid, so it is better to retrieve them once rather than in every single call to thewidgetHandlefunction.The block of code that sets the variables and calls
Note: When extending the behavior of an object or a class of objects using a super procedure, be sensitive to making the additional behavior both optional and efficient.rowDisplayfirst checks to see whether that procedure has been implemented at all. This is always good practice when you are extending the behavior of any object or class. This could be stated as a second super procedure principle.In this case, the specific
rowDisplayfor a viewer will probably be implemented in a custom super procedure for that viewer. Some viewers might have this procedure and some might not. So the behavior is made optional here because the statement toRUNrowDisplayis doneNO-ERRORso that there is no error if it is not defined in theTARGET-PROCEDURE. Simply adding aNO-ERRORqualifier on aRUNstatement will often be sufficient to make sure that your extension will not break the code in objects that do not use your extension.Note that you cannot check for
rowDisplayby looking for it in the 4GL procedure handle attributeTARGET-PROCEDURE:INTERNAL-ENTRIESbecause this Progress attribute returns only those entry points that are actually coded in theTARGET-PROCEDURE, or for which there are prototypes compiled into the procedure. In this case, theTARGET-PROCEDUREis the viewer procedure, androwDisplaywill normally be implemented in its custom super procedure, so the entry point won’t be found, as shown in Figure 1–4.There is one more critical detail in the
Note: Always remember to invoke internal procedures and functionsRUNstatement, and that is the reference toTARGET-PROCEDURE. This brings up the third key point in working with super procedures.INTARGET-PROCEDUREfrom a super procedure. The same applies toPUBLISHandSUBSCRIBEstatements in super procedures.This is one of the most common errors made when people write code for super procedures or, even worse, when people move code from an object procedure such as a static viewer to a super procedure that supports it. Code that worked fine before can simply stop working (that is, stop being executed at all) for this reason. Always keep this relationship between object and super procedure in mind.
Figure 1–4 shows the relationships between the procedures in this case.
Figure 1–4: Relationships between objects and super procedures
![]()
In addition to the standard set of super procedures starting with
smart.pand ending withviewer.p, theCustomerviewer has two additional super procedures,viewercustom.p, which applies to the whole class, andcustomersuper.p, which applies just to the one viewer. This is the case whether the base object procedure is a static viewer such as custstviewv.w, or an instance of the generic dynamic viewer procedure rydynviewv.w. Any statement invokedINTARGET-PROCEDUREstarts at the viewer procedure itself and works its way up the stack until it finds an implementation of the entry point being run. If this entry point has aRUNSUPERstatement in it, then it continues up the object’s super procedure stack looking for the next version of it.Note that the super procedures themselves are not super procedures of each other. So any call that wants to find an implementation of an entry point in the stack must be made
INTARGET-PROCEDURE. TheTARGET-PROCEDUREof the customer-specific super procedurecustomersuper.pis the viewer itself. TheTARGET-PROCEDUREof the genericviewercustom.pis likewise the viewer itself.The statement
RUNrowDisplayINTARGET-PROCEDUREattempts to run it in the viewer procedure. According to recommended coding practice,rowDisplaywill not be found there because there is no custom code in the viewer itself, even if it is a static procedure. But the Progress interpreter then looks up the viewer’s super procedure stack and findsrowDisplayincustomersuper.p, and executes it there.This leads to an interesting problem with this structure where 4GL functions are concerned. The Progress compiler must have a function definition for any static function reference in a procedure’s code. The only alternative to this is to use the
DYNAMIC-FUNCTIONsyntax, which is rather clumsy for a model where you want the simplest possible coding style in your client logic.So you want to be able to reference functions like
widgetHandledirectly in code in customersuper.p, even though the functions are no longer defined there. To do this without error, you must include function prototypes of all the support functions in your super procedure.There is an OpenEdge tool to generate the prototypes for you, called ProtoGen. It is available from the ProTools palette, as shown in Figure 1–5.
Figure 1–5: ProTools palette
![]()
In the Prototype Generator, you first enter the name of the super procedure it should generate prototypes for. In this case, it is the local version of
src/adm2/custom/viewercustom.p. You then enter the name of the include file that the tool generates containing the prototype definitions. In the example it should be calledviewcustomprto.i, as shown in Figure 1–6.Figure 1–6: Prototype Generator window
![]()
Select the Generate button to create the include file.
In its default form, however, it is not exactly what you need. This is because the generator assumes that the include file will be added to an object procedure, such as the viewer procedure, so that functions in its super procedures can be referenced. For this reason, it creates function prototypes defined as being
INSUPER, meaning that the interpreter should invoke the function, and it will be found in a super procedure. For example:
But in the present case, the function references are in another super procedure, viewercustom.p, which, as explained, is not a super procedure of customersuper.p. So the function prototypes must be modified to tell the interpreter to execute the function
INTARGET-PROCEDURE. When this happens, the interpreter searches up the stack and finds the function prototypes in another super procedure of the viewer, namely viewercustom.p, as shown:
Note: You can remove the prototype for the internal procedure
displayFields, which is not needed. This is what the resulting include file should look like. The exact function prototypes depend on how many supporting functions you have in viewercustom.p.Next, modify the Definitions section of your
customersuper.pto include the prototype file, as shown:
Now any references to the supporting functions in your custom super procedure will compile and execute successfully.
This entire exercise of moving commonly used code into a higher level in the super procedure stack illustrates a fourth principle to keep in mind whenever you are developing an application.
Note: Always factor out common code into the highest possible point in the super procedure stack.A key aspect of Progress Dynamics as an application framework is that it provides a great deal of standard behavior that can be used in all objects of a given type. Whenever you develop code of your own, try to use it to extend that standard behavior whenever possible. In a sense this is just good normal programming practice, employing the concept of modular programming. However, the Progress 4GL with its super procedures, published events, and object attributes provides a specific structure within which you can implement modular code. Whenever you write 4GL code, always consider what general use it can be, and always take the time to remove commonly useful code and move it up to a higher level in the tree. When you do this, consider the range of objects that might want to take advantage of it. Does the code apply to just a subset of viewers, for example, so that it would be worthwhile to create a new specialized viewer class for just those objects? Or does it apply to all viewers? To all
Datavisobjects, which are visual objects that display database data? To all visual objects? To all client-side objects? This same set of questions can be asked about behavior you are adding to any part of the overall SmartObject class tree. Taking the time to write code that can then be reused by many other objects is an investment that will pay off many times over. The continuation of the viewer event example below is another illustration of this.If you look at the UI event example, you will see that the supporting code needs further refinement. In the case of the
rowDisplay, this is always called in the context of a single row’s display event, and putting code intodisplayFieldsto set and unset the local context (the values ofAllFieldNamesandAllFieldHandles) can safely be done there. But a UI event can occur on any event for any widget, so it is not as easy to encapsulate the context setting in such a general way.The code for the example event procedure
CreditLimitLeavemust be modified to set and unset the context. Because this might be happening all over the place in your code, you should extract the code out into another pair of functions implemented in viewercustom.p, which you can callsetDisplayContextandclearDisplayContext,as shown in the following example:
Now the
displayFieldsprocedure in viewercustom.p can be cleaned up a little to use these two functions, as shown:
So why does the reference to
setDisplayContexthave to executeINTARGET-PROCEDURE(which in turn requires the use of theDYNAMIC-FUNCTIONsyntax, which allows you to specify where the function is invoked), even though thesetDisplayContextfunction is implemented in the same source procedure as this version ofdisplayFields? It is because of another aspect in the use ofTARGET-PROCEDUREthat it is important to go over.In addition to using
INTARGET-PROCEDUREin code references that are written in the object the code supports, or that are written in another super procedure in the stack, it is important to remember to useINTARGET-PROCEDUREeven if the entry point your code is running is implemented in the super procedure itself, if the entry point makes another reference to theTARGET-PROCEDURE. This is because theTARGET-PROCEDUREreference in the called entry point only evaluates properly if it has bounced back from the base object itself. If the entry point is instead invoked directly inTHIS-PROCEDURE(which is implicit if there is no qualifier on theRUNstatement or function invocation), then within the called entry point, the value ofTARGET-PROCEDUREis the same asTHIS-PROCEDURE, since that is where the call originated.Figure 1–7 illustrates the difference between the references.
Figure 1–7: Improper function reference without TARGET-PROCEDURE
![]()
In contrast, the
setDisplayContextreference in the example uses the special{get}pseudo-syntax to retrieve the attribute values, and this includes the necessary reference toTARGET-PROCEDUREto point at the object where the attribute is defined, so any reference to the function must itself be madeINTARGET-PROCEDUREfor things to resolve properly.So with the function reference explicitly made
INTARGET-PROCEDURE, the function itself will be able to resolve it properly, as Figure 1–8 shows.Figure 1–8: Proper function reference with TARGET-PROCEDURE
![]()
Now each event procedure sets and clears the context, so all references to the fields and handles within all invoked functions resolve properly without the attributes being retrieved each time, as shown:
![]()
To bring all this into sync:
- Make the changes to the
viewercustom.pprocedure, save it, and compile it.- Regenerate the
viewcustomprto.iprototype include file using the ProtoGen tool.- Edit the
viewcustomprto.ifile to changeINSUPERtoINTARGET-PROCEDURE.- Compile all the viewers that must use this support procedure. This includes any static viewers, such as
custstviewv.wfrom the static example, as well as the generic dynamic viewer procedurerydynviewv.w.As noted before, the formula for the credit limit check itself, which is duplicated in
rowDisplayandCreditLimitLeave, should be removed to another function where it only must be defined once.There is another way in which this example is simplified to the point where it does not actually set a good example: The formula that says that a customer balance within $5,000 of the credit limit represents a warning condition is really business logic, even though the way that condition is dealt with is simply to change something in the user interface. If that logic must be changed, or modified depending on the user organization or other conditions, then you have a maintenance headache to deal with because the code that expresses the condition is compiled into client user interface procedures and deployed to all the client systems.
It is much better to bring as much of the logic as possible over from the server at run time. At a minimum, this means that the $5,000 figure should be turned into an application property that is retrieved as needed, perhaps at startup along with a whole set of other client logic properties, in a single call to a business logic procedure running on the server. In that way, the logic that maintains that figure and determines what the proper figure is for this user or this organization can be maintained on the server along with other business logic.
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |